Skip to content

S3Client: don't double-deref path when a method fails after blob construction#30419

Closed
robobun wants to merge 4 commits into
mainfrom
farm/9135a43e/fix-s3-presign-double-deref
Closed

S3Client: don't double-deref path when a method fails after blob construction#30419
robobun wants to merge 4 commits into
mainfrom
farm/9135a43e/fix-s3-presign-double-deref

Conversation

@robobun

@robobun robobun commented May 9, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes a crash in S3Client methods that take a path (presign, write, exists, size, stat, unlink — both static and instance) when the operation fails synchronously after the blob store is constructed.

Bun.S3Client.presign("myfile");           // missing credentials
new Bun.S3Client({}).write("myfile", null); // null data
// panic(main thread): reached unreachable code
//   bun.assert(self.hasAtLeastOneRef())

Why

constructS3FileInternalStore / constructS3FileWithS3CredentialsAndOptions hand the PathLike to Blob.Store.initS3, which calls toThreadSafe() — a transfer of ownership of the underlying WTFStringImpl to the store (no extra ref is taken despite the misleading comment). The caller's path handle still aliases the same impl.

When a subsequent operation in the caller returns a Zig error (getPresignUrlFromerror.MissingCredentials, Blob.writeFileInternalthrowInvalidArguments on null data, etc.), both defer blob.deinit() and the outer errdefer path.deinit() run, deref'ing the same string twice. In debug this trips hasAtLeastOneRef(); in release it aborts or corrupts the refcount.

There was also a secondary instance inside the helpers themselves: after initS3, they re-read options.type from JS, so a getter that throws on the second access would hit the same double-deref via errdefer store.deinit().

How

  • Clear the caller's path handle to .{ .string = bun.PathString.empty } once ownership has moved to the blob, so the errdefer becomes a no-op (.string variant of PathLike.deinit does nothing). Matches the existing pattern in Blob.findOrCreateFileFromPath. Applied to presign/write/exists/size/stat/unlink in both S3Client.zig and S3File.zig.
  • In the construction helpers, reuse the content_type already parsed by getCredentialsWithOptions instead of re-reading options.type, so nothing fallible runs after initS3. As a side effect options.type is read once instead of twice.

Found by Fuzzilli. Fingerprint 434aafd9d16f9f0a.

constructS3FileInternalStore / constructS3FileWithS3CredentialsAndOptions
pass the PathLike to Blob.Store.initS3 which calls toThreadSafe(),
transferring ownership of the underlying WTFStringImpl to the store.
When getPresignUrlFrom subsequently fails (e.g. missing credentials),
both `defer blob.deinit()` and the outer `errdefer path.deinit()` ran,
deref'ing the same string twice and tripping the hasAtLeastOneRef assert
(or corrupting the refcount in release builds).

Clear the caller's path handle once ownership has moved to the blob,
matching the pattern in Blob.findOrCreateFileFromPath.
@github-actions github-actions Bot added the claude label May 9, 2026
@robobun

robobun commented May 9, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:07 AM PT - May 9th, 2026

@robobun, your commit a044067 has 2 failures in Build #52994 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30419

That installs a local version of the PR into your bun-30419 executable, so you can run:

bun-30419 --bun

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Defaults presign/path branches to an empty PathString after consuming options, centralizes blob content-type assignment in S3File constructors via setBlobContentType, removes local store errdefer cleanup patterns, and adds a test for missing S3 credentials.

Changes

S3 Presign Path Handling and Constructors Refactor

Layer / File(s) Summary
Path handling
src/runtime/webcore/S3Client.zig, src/runtime/webcore/S3File.zig
Parsed path variables are made mutable and set to an empty bun.PathString (or .path) after options are consumed and before constructing the internal S3 blob/store for presign, exists, size, stat, write, and unlink.
S3File Constructors / Blob Setup
src/runtime/webcore/S3File.zig
Removed local errdefer store.deinit() cleanup in constructors and replaced inline JS options "type" parsing with a centralized call to setBlobContentType(…, aws_options.content_type, globalObject).
Blob Content-Type Helper
src/runtime/webcore/S3File.zig
Added setBlobContentType helper that validates ASCII, consults globalObject.bunVM().mimeType, or lowercases/copies and allocates the provided content_type into the blob and marks allocation state.
Missing Credentials Error Test
test/js/bun/s3/s3-presign-missing-credentials.test.ts
New test spawns a Bun process with S3/AWS credential env vars cleared, invokes presign/write variants, and asserts the expected thrown error codes with empty stderr and controlled stdout/exit.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and specifically describes the main bug fix: preventing a double-dereference of the path variable in S3Client methods when errors occur after blob construction.
Description check ✅ Passed The description comprehensively covers the What (the crash being fixed), Why (detailed root cause analysis of the double-dereference), and How (the solution approach), exceeding the template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/webcore/S3File.zig`:
- Around line 85-89: The path ownership must be cleared at the moment the helper
takes ownership to avoid double-free if the helper later throws; modify
constructS3FileInternalStore (or create an overload that accepts a mutable
PathLike) so it blanks the incoming PathString/PathLike immediately when
Blob.Store.initS3 takes ownership (i.e., right after the transfer) rather than
relying on the caller to clear path_or_blob; ensure the helper returns the
initialized blob with the original path cleared (or returns an error without
double-deref) so callers of constructS3FileInternalStore and callers of
getPresignUrlFrom won't end up deinitializing the same PathString twice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a44a4a09-e690-4628-a870-88feaa8496d7

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0d86b and 83c898a.

📒 Files selected for processing (3)
  • src/runtime/webcore/S3Client.zig
  • src/runtime/webcore/S3File.zig
  • test/js/bun/s3/s3-presign-missing-credentials.test.ts

Comment thread src/runtime/webcore/S3File.zig
@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. s3: fix path double-free when presign throws after blob creation #30351 - Fixes same S3 path double-free in S3Client.zig/S3File.zig by passing path as mutable pointer to clear after ownership transfer
  2. fix(s3): double free of path when S3Client operation throws #29656 - Fixes same double-free by removing errdefer on path and nullifying path_or_blob after constructS3FileInternalStore takes ownership
  3. fix(s3): don't double-free path when S3Client static ops throw after blob creation #29081 - Fixes same double-free in S3File.zig by setting path_or_blob to fd sentinel after ownership transfer
  4. Fix double-free in S3 static methods when path is passed as string #28592 - Fixes same double-free by reassigning path_or_blob to blob and clearing path after ownership transfer
  5. Fix double-free of path in S3 static methods on error paths #28495 - Fixes same double-free by clearing path after ownership transfer and neutralizing errdefer
  6. Fix crash in S3 presign with missing credentials #28423 - Fixes same presign crash with missing credentials by refactoring to avoid blob construction entirely
  7. Fix use-after-free in S3 Store.initS3 PathLike refcounting #28417 - Fixes same use-after-free by neutralizing path_or_blob after ownership transfer and removing errdefer

🤖 Generated with Claude Code

getCredentialsWithOptions already reads and validates options.type into
aws_options.content_type. Re-reading it after initS3 was the only fallible
operation past the point where the store takes ownership of the path; a
getter that throws on the second access would trip the same double-deref
as the presign case. Use the already-parsed slice and drop the now-dead
errdefer store.deinit(). As a side effect options.type is read once
instead of twice.
Comment thread src/runtime/webcore/S3Client.zig
write() was hitting the same double-deref when data is null/undefined:
Blob.writeFileInternal throws synchronously before creating a promise,
so both defer blob.detach() and errdefer path.deinit() ran. exists/size/
stat/unlink have fallible tails too. Apply the same post-transfer clear
everywhere the helper hands the path to the store.
@robobun robobun changed the title S3Client.presign: don't double-deref path on sign error S3Client: don't double-deref path when a method fails after blob construction May 9, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — c8149ec addresses my earlier feedback by applying the post-transfer path clear to all siblings in both files, and the helper refactor makes the fail-before-transfer-or-succeed invariant hold.

Extended reasoning...

Overview

This PR fixes a double-deref crash in S3Client/S3File methods (presign, write, exists, size, stat, unlink) when an operation fails synchronously after the blob store has taken ownership of the path's WTFStringImpl. The fix is two-pronged: (1) clear the caller's path handle to .{ .string = bun.PathString.empty } immediately after the construction helper returns, so the outer errdefer path.deinit() becomes a no-op; (2) refactor the construction helpers to reuse aws_options.content_type (already parsed by getCredentialsWithOptions) instead of re-reading options.type from JS, which removes the only fallible operation after initS3 and lets the errdefer store.deinit() be dropped. A regression test spawns a subprocess with credentials cleared and asserts the expected error codes with no stderr.

Follow-up on prior review

My earlier inline comment flagged that write (and the other siblings) could throw synchronously after blob construction, hitting the same double-deref. The author confirmed and applied the same one-line clear to all six methods in both S3Client.zig (instance) and S3File.zig (static) in c8149ec, and extended the test to cover write(path, null). I verified the diff: all 12 call sites now clear the path immediately after the helper returns. The author's note that file is safe (toJS infallible) and listObjects already passes .string = empty checks out.

Security risks

None. This is a memory-safety fix (refcount underflow / use-after-free on a WTF string) — it reduces attack surface rather than adding any. No auth, crypto, or input-validation logic is touched.

Level of scrutiny

Moderate. This is refcount/lifetime management in the runtime, which is subtle territory, but the fix follows an established pattern (Blob.findOrCreateFileFromPath does the same path-clear after ownership transfer) and is mechanically applied. I verified that initWithStore returns Blob (not an error union) and setBlobContentType returns void with only bun.handleOom (abort-on-OOM), so the helper's new contract — fail before initS3, or succeed with nothing fallible after — holds. The aws_options.content_type slice is only read (copied or mapped to a static MIME entry) before defer aws_options.deinit() runs, so no lifetime issue there.

Other factors

CodeRabbit's earlier concern (clearing at the ownership-transfer point) was addressed by making the helper infallible post-initS3 rather than threading *PathLike through ~20 call sites — a cleaner approach. The two CI failures (s3-storage-class.test.ts on a single macOS shard, test-http-should-emit-close-when-connection-is-aborted.ts timeout on Windows) are unrelated to this change. The duplicate-PR bot lists several competing fixes for the same fingerprint; that's a merge-coordination question, not a code-correctness one.

@robobun

robobun commented May 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the remaining failures are pre-existing flakes unrelated to this change, present on recently-merged PRs as well.

Test Platform Also failing on merged PRs
test-http-should-emit-close-when-connection-is-aborted.ts Windows ×2–3 #30398, #30404, #30408
s3-storage-class.test.tswriter + options on big file darwin (varies) #30404, #30389, #30368
bun-install-registry.test.tshoisting peers Windows aarch64 flaky hoist resolution

The s3-storage-class failure is in the .writer() multipart path (not touched here) and passes locally and on all other platforms; it also passed on retry in build 52994. debian-13-x64-asan passed. The new s3-presign-missing-credentials.test.ts passes everywhere.

Already re-rolled once (a044067); not retriggering again.

@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #30495 (same fix against current main, minimal diff, deterministic test).

@robobun robobun closed this May 11, 2026
@robobun robobun deleted the farm/9135a43e/fix-s3-presign-double-deref branch May 11, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant